/**
 ******************************************************************************
 * @file       pios_flashfs_logfs.c
 * @author     PhoenixPilot, http://github.com/PhoenixPilot, Copyright (C) 2012
 * @addtogroup PIOS PIOS Core hardware abstraction layer
 * @{
 * @addtogroup PIOS_FLASHFS Flash Filesystem Function
 * @{
 * @brief Log Structured Filesystem for internal or external NOR Flash
 *****************************************************************************/
/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */


#include "pios.h"

#ifdef PIOS_INCLUDE_FLASH

#include <stdbool.h>
#include <openpilot.h>
#include <pios_math.h>
#include <pios_wdg.h>
#include "pios_flashfs_logfs_priv.h"

/*
 * Filesystem state data tracked in RAM
 */

enum pios_flashfs_logfs_dev_magic {
    PIOS_FLASHFS_LOGFS_DEV_MAGIC = 0x94938201,
};

struct logfs_state {
    enum pios_flashfs_logfs_dev_magic magic;
    const struct flashfs_logfs_cfg    *cfg;
    bool    mounted;
    uint8_t active_arena_id;

    /* NOTE: num_active_slots + num_free_slots will not typically add
     *       up to the number of slots in the arena since some of the
     *       slots will be obsolete or otherwise invalidated
     */
    uint16_t num_free_slots; /* slots in free state */
    uint16_t num_active_slots; /* slots in active state */

    /* Underlying flash driver glue */
    const struct pios_flash_driver *driver;
    uintptr_t flash_id;
};

/*
 * Internal Utility functions
 */

/**
 * @brief Return the offset in flash of a particular slot within an arena
 * @return address of the requested slot
 */
static uintptr_t logfs_get_addr(const struct logfs_state *logfs, uint8_t arena_id, uint16_t slot_id)
{
    PIOS_Assert(arena_id < (logfs->cfg->total_fs_size / logfs->cfg->arena_size));
    PIOS_Assert(slot_id < (logfs->cfg->arena_size / logfs->cfg->slot_size));

    return logfs->cfg->start_offset +
           (arena_id * logfs->cfg->arena_size) +
           (slot_id * logfs->cfg->slot_size);
}

/*
 * The bits within these enum values must progress ONLY
 * from 1 -> 0 so that we can write later ones on top
 * of earlier ones in NOR flash without an erase cycle.
 */
enum arena_state {
    /*
     * The STM32F30X flash subsystem is only capable of
     * writing words or halfwords. In this case we use halfwords.
     * In addition to that it is only capable to write to erased
     * cells (0xffff) or write a cell from anything to (0x0000).
     * To cope with this, the F3 needs carefully crafted enum values.
     * For this to work the underlying flash driver has to
     * check each halfword if it has changed before writing.
     */
    ARENA_STATE_ERASED   = 0xFFFFFFFF,
    ARENA_STATE_RESERVED = 0xE6E6FFFF,
    ARENA_STATE_ACTIVE   = 0xE6E66666,
    ARENA_STATE_OBSOLETE = 0x00000000,
};

struct arena_header {
    uint32_t magic;
    enum arena_state state;
} __attribute__((packed));


/****************************************
* Arena life-cycle transition functions
****************************************/

/**
 * @brief Erases all sectors within the given arena and sets arena to erased state.
 * @return 0 if success, < 0 on failure
 * @note Must be called while holding the flash transaction lock
 */
static int32_t logfs_erase_arena(const struct logfs_state *logfs, uint8_t arena_id)
{
    uintptr_t arena_addr = logfs_get_addr(logfs, arena_id, 0);

    /* Erase all of the sectors in the arena */
    for (uint8_t sector_id = 0;
         sector_id < (logfs->cfg->arena_size / logfs->cfg->sector_size);
         sector_id++) {
        if (logfs->driver->erase_sector(logfs->flash_id,
                                        arena_addr + (sector_id * logfs->cfg->sector_size))) {
            return -1;
        }
    }

    /* Mark this arena as fully erased */
    struct arena_header arena_hdr = {
        .magic = logfs->cfg->fs_magic,
        .state = ARENA_STATE_ERASED,
    };

    if (logfs->driver->write_data(logfs->flash_id,
                                  arena_addr,
                                  (uint8_t *)&arena_hdr,
                                  sizeof(arena_hdr)) != 0) {
        return -2;
    }

    /* Arena is ready to be activated */
    return 0;
}

/**
 * @brief Marks the given arena as reserved so it can be filled.
 * @return 0 if success, < 0 on failure
 * @note Arena must have been previously erased before calling this
 * @note Must be called while holding the flash transaction lock
 */
static int32_t logfs_reserve_arena(const struct logfs_state *logfs, uint8_t arena_id)
{
    uintptr_t arena_addr = logfs_get_addr(logfs, arena_id, 0);

    /* Read in the current arena header */
    struct arena_header arena_hdr;

    if (logfs->driver->read_data(logfs->flash_id,
                                 arena_addr,
                                 (uint8_t *)&arena_hdr,
                                 sizeof(arena_hdr)) != 0) {
        return -1;
    }
    if (arena_hdr.state != ARENA_STATE_ERASED) {
        /* Arena was not erased, can't reserve it */
        return -2;
    }

    /* Set the arena state to reserved */
    arena_hdr.state = ARENA_STATE_RESERVED;

    /* Write the arena header back to flash */
    if (logfs->driver->write_data(logfs->flash_id,
                                  arena_addr,
                                  (uint8_t *)&arena_hdr,
                                  sizeof(arena_hdr)) != 0) {
        return -3;
    }

    /* Arena is ready to be filled */
    return 0;
}

/**
 * @brief Erases all arenas available to this filesystem instance
 * @return 0 if success, < 0 on failure
 * @note Must be called while holding the flash transaction lock
 */
static int32_t logfs_erase_all_arenas(const struct logfs_state *logfs)
{
    uint16_t num_arenas = logfs->cfg->total_fs_size / logfs->cfg->arena_size;

    for (uint16_t arena = 0; arena < num_arenas; arena++) {
#ifdef PIOS_LED_HEARTBEAT
        PIOS_LED_Toggle(PIOS_LED_HEARTBEAT);
#endif
#ifdef PIOS_INCLUDE_WDG
        PIOS_WDG_Clear();
#endif
        if (logfs_erase_arena(logfs, arena) != 0) {
            return -1;
        }
    }

    return 0;
}

/**
 * @brief Marks the given arena as active so it can be mounted.
 * @return 0 if success, < 0 on failure
 * @note Arena must have been previously erased or reserved before calling this
 * @note Must be called while holding the flash transaction lock
 */
static int32_t logfs_activate_arena(const struct logfs_state *logfs, uint8_t arena_id)
{
    uintptr_t arena_addr = logfs_get_addr(logfs, arena_id, 0);

    /* Make sure this arena has been previously erased */
    struct arena_header arena_hdr;

    if (logfs->driver->read_data(logfs->flash_id,
                                 arena_addr,
                                 (uint8_t *)&arena_hdr,
                                 sizeof(arena_hdr)) != 0) {
        /* Failed to read arena header */
        return -1;
    }
    if ((arena_hdr.state != ARENA_STATE_RESERVED) &&
        (arena_hdr.state != ARENA_STATE_ERASED)) {
        /* Arena was not erased or reserved, can't activate it */
        return -2;
    }

    /* Mark this arena as active */
    arena_hdr.state = ARENA_STATE_ACTIVE;
    if (logfs->driver->write_data(logfs->flash_id,
                                  arena_addr,
                                  (uint8_t *)&arena_hdr,
                                  sizeof(arena_hdr)) != 0) {
        return -3;
    }

    /* The arena is now activated and the log may be mounted */
    return 0;
}

/**
 * @brief Marks the given arena as obsolete.
 * @return 0 if success, < 0 on failure
 * @note Arena must have been previously active before calling this
 * @note Must be called while holding the flash transaction lock
 */
static int32_t logfs_obsolete_arena(const struct logfs_state *logfs, uint8_t arena_id)
{
    uintptr_t arena_addr = logfs_get_addr(logfs, arena_id, 0);

    /* We shouldn't be retiring the currently active arena */
    PIOS_Assert(!logfs->mounted);

    /* Make sure this arena was previously active */
    struct arena_header arena_hdr;
    if (logfs->driver->read_data(logfs->flash_id,
                                 arena_addr,
                                 (uint8_t *)&arena_hdr,
                                 sizeof(arena_hdr)) != 0) {
        /* Failed to read arena header */
        return -1;
    }

    if (arena_hdr.state != ARENA_STATE_ACTIVE) {
        /* Arena was not previously active, can't obsolete it */
        return -2;
    }

    /* Mark this arena as obsolete */
    arena_hdr.state = ARENA_STATE_OBSOLETE;
    if (logfs->driver->write_data(logfs->flash_id,
                                  arena_addr,
                                  (uint8_t *)&arena_hdr,
                                  sizeof(arena_hdr)) != 0) {
        return -3;
    }

    /* Arena is now obsoleted */
    return 0;
}

/**
 * @brief Find the first active arena in flash
 * @return arena_id (>=0) of first active arena
 * @return -1 if no active arena is found
 * @return -2 if failed to read arena header
 * @note Must be called while holding the flash transaction lock
 */
static int32_t logfs_find_active_arena(const struct logfs_state *logfs)
{
    /* Search for the lowest numbered active arena */
    for (uint8_t arena_id = 0;
         arena_id < logfs->cfg->total_fs_size / logfs->cfg->arena_size;
         arena_id++) {
        uintptr_t arena_addr = logfs_get_addr(logfs, arena_id, 0);
        /* Load the arena header */
        struct arena_header arena_hdr;
        if (logfs->driver->read_data(logfs->flash_id,
                                     arena_addr,
                                     (uint8_t *)&arena_hdr,
                                     sizeof(arena_hdr)) != 0) {
            return -2;
        }
        if ((arena_hdr.state == ARENA_STATE_ACTIVE) &&
            (arena_hdr.magic == logfs->cfg->fs_magic)) {
            /* This is the first active arena */
            return arena_id;
        }
#ifdef PIOS_INCLUDE_WDG
        PIOS_WDG_Clear();
#endif
    }

    /* Didn't find an active arena */
    return -1;
}

/*
 * The bits within these enum values must progress ONLY
 * from 1 -> 0 so that we can write later ones on top
 * of earlier ones in NOR flash without an erase cycle.
 */
enum slot_state {
    /*
     * The STM32F30X flash subsystem is only capable of
     * writing words or halfwords. In this case we use halfwords.
     * In addition to that it is only capable to write to erased
     * cells (0xffff) or write a cell from anything to (0x0000).
     * To cope with this, the F3 needs carfully crafted enum values.
     * For this to work the underlying flash driver has to
     * check each halfword if it has changed before writing.
     */
    SLOT_STATE_EMPTY    = 0xFFFFFFFF,
    SLOT_STATE_RESERVED = 0xFAFAFFFF,
    SLOT_STATE_ACTIVE   = 0xFAFAAAAA,
    SLOT_STATE_OBSOLETE = 0x00000000,
};

struct slot_header {
    enum slot_state state;
    uint32_t obj_id;
    uint16_t obj_inst_id;
    uint16_t obj_size;
} __attribute__((packed));

/* NOTE: Must be called while holding the flash transaction lock */
static int32_t logfs_raw_copy_bytes(const struct logfs_state *logfs, uintptr_t src_addr, uint16_t src_size, uintptr_t dst_addr)
{
#define RAW_COPY_BLOCK_SIZE 16
    uint8_t data_block[RAW_COPY_BLOCK_SIZE];

    while (src_size) {
        uint16_t blk_size;
        if (src_size >= RAW_COPY_BLOCK_SIZE) {
            /* Copy a full block */
            blk_size = RAW_COPY_BLOCK_SIZE;
        } else {
            /* Copy the remainder */
            blk_size = src_size;
        }

        /* Read a block of data from source */
        if (logfs->driver->read_data(logfs->flash_id,
                                     src_addr,
                                     data_block,
                                     blk_size) != 0) {
            /* Failed to read next chunk from source */
            return -1;
        }

        /* Write a block of data to destination */
        if (logfs->driver->write_data(logfs->flash_id,
                                      dst_addr,
                                      data_block,
                                      blk_size) != 0) {
            /* Failed to write chunk to destination */
            return -2;
        }

        /* Update the src/dst pointers */
        src_size -= blk_size;
        src_addr += blk_size;
        dst_addr += blk_size;
    }

    return 0;
}

/*
 * Is the entire filesystem full?
 * true = all slots in the arena are in the ACTIVE state (ie. garbage collection won't free anything)
 * false = some slots in the arena are either currently free or could be free'd by garbage collection
 */
static bool logfs_fs_is_full(const struct logfs_state *logfs)
{
    return logfs->num_active_slots == (logfs->cfg->arena_size / logfs->cfg->slot_size) - 1;
}

/*
 * Is the log full?
 * true = there are no unwritten slots left in the log (garbage collection may or may not help)
 * false = there are still some entirely unused slots left in the log
 */
static bool logfs_log_is_full(const struct logfs_state *logfs)
{
    return logfs->num_free_slots == 0;
}

static int32_t logfs_unmount_log(struct logfs_state *logfs)
{
    PIOS_Assert(logfs->mounted);

    logfs->num_active_slots = 0;
    logfs->num_free_slots   = 0;
    logfs->mounted = false;

    return 0;
}

static int32_t logfs_mount_log(struct logfs_state *logfs, uint8_t arena_id)
{
    PIOS_Assert(!logfs->mounted);

    logfs->num_active_slots = 0;
    logfs->num_free_slots   = 0;
    logfs->active_arena_id  = arena_id;

    /* Scan the log to find out how full it is */
    for (uint16_t slot_id = 1;
         slot_id < (logfs->cfg->arena_size / logfs->cfg->slot_size);
         slot_id++) {
        struct slot_header slot_hdr;
        uintptr_t slot_addr = logfs_get_addr(logfs, logfs->active_arena_id, slot_id);
        if (logfs->driver->read_data(logfs->flash_id,
                                     slot_addr,
                                     (uint8_t *)&slot_hdr,
                                     sizeof(slot_hdr)) != 0) {
            return -1;
        }

        /*
         * Empty slots must be in a continguous block at the
         * end of the arena.
         */
        PIOS_Assert(slot_hdr.state == SLOT_STATE_EMPTY ||
                    logfs->num_free_slots == 0);

        switch (slot_hdr.state) {
        case SLOT_STATE_EMPTY:
            logfs->num_free_slots++;
            break;
        case SLOT_STATE_ACTIVE:
            logfs->num_active_slots++;
            break;
        case SLOT_STATE_RESERVED:
        case SLOT_STATE_OBSOLETE:
            break;
        }
    }

    /* Scan is complete, mark the arena mounted */
    logfs->active_arena_id = arena_id;
    logfs->mounted = true;

    return 0;
}

static bool PIOS_FLASHFS_Logfs_validate(const struct logfs_state *logfs)
{
    return logfs && (logfs->magic == PIOS_FLASHFS_LOGFS_DEV_MAGIC);
}

#if defined(PIOS_INCLUDE_FREERTOS)
static struct logfs_state *PIOS_FLASHFS_Logfs_alloc(void)
{
    struct logfs_state *logfs;

    logfs = (struct logfs_state *)pios_malloc(sizeof(*logfs));
    if (!logfs) {
        return NULL;
    }

    logfs->magic = PIOS_FLASHFS_LOGFS_DEV_MAGIC;
    return logfs;
}
static void PIOS_FLASHFS_Logfs_free(struct logfs_state *logfs)
{
    /* Invalidate the magic */
    logfs->magic = ~PIOS_FLASHFS_LOGFS_DEV_MAGIC;
    vPortFree(logfs);
}
#else
static struct logfs_state pios_flashfs_logfs_devs[PIOS_FLASHFS_LOGFS_MAX_DEVS];
static uint8_t pios_flashfs_logfs_num_devs;
static struct logfs_state *PIOS_FLASHFS_Logfs_alloc(void)
{
    struct logfs_state *logfs;

    if (pios_flashfs_logfs_num_devs >= PIOS_FLASHFS_LOGFS_MAX_DEVS) {
        return NULL;
    }

    logfs = &pios_flashfs_logfs_devs[pios_flashfs_logfs_num_devs++];
    logfs->magic = PIOS_FLASHFS_LOGFS_DEV_MAGIC;

    return logfs;
}
static void PIOS_FLASHFS_Logfs_free(struct logfs_state *logfs)
{
    /* Invalidate the magic */
    logfs->magic = ~PIOS_FLASHFS_LOGFS_DEV_MAGIC;

    /* Can't free the resources with this simple allocator */
}
#endif /* if defined(PIOS_INCLUDE_FREERTOS) */

/**
 * @brief Initialize the flash object setting FS
 * @return 0 if success, -1 if failure
 */
int32_t PIOS_FLASHFS_Logfs_Init(uintptr_t *fs_id, const struct flashfs_logfs_cfg *cfg, const struct pios_flash_driver *driver, uintptr_t flash_id)
{
    PIOS_Assert(cfg);
    PIOS_Assert(fs_id);
    PIOS_Assert(driver);

    /* We must have at least 2 arenas for garbage collection to work */
    PIOS_Assert((cfg->total_fs_size / cfg->arena_size > 1));

    /* Make sure the underlying flash driver provides the minimal set of required methods */
    PIOS_Assert(driver->start_transaction);
    PIOS_Assert(driver->end_transaction);
    PIOS_Assert(driver->erase_sector);
    PIOS_Assert(driver->write_data);
    PIOS_Assert(driver->read_data);

    int8_t rc;

    struct logfs_state *logfs;

    logfs = (struct logfs_state *)PIOS_FLASHFS_Logfs_alloc();
    if (!logfs) {
        rc = -1;
        goto out_exit;
    }

    /* Bind configuration parameters to this filesystem instance */
    logfs->cfg      = cfg;  /* filesystem configuration */
    logfs->driver   = driver; /* lower-level flash driver */
    logfs->flash_id = flash_id; /* lower-level flash device id */
    logfs->mounted  = false;

    if (logfs->driver->start_transaction(logfs->flash_id) != 0) {
        rc = -1;
        goto out_exit;
    }

    bool found = false;
    int32_t arena_id;
    for (uint8_t try = 0; !found && try < 2; try++) {
        /* Find the active arena */
        arena_id = logfs_find_active_arena(logfs);
        if (arena_id >= 0) {
            /* Found the active arena */
            found = true;
            break;
        } else {
            /* No active arena found, erase and activate arena 0 */
            if (logfs_erase_arena(logfs, 0) != 0) {
                break;
            }
            if (logfs_activate_arena(logfs, 0) != 0) {
                break;
            }
        }
    }

    if (!found) {
        /* Still no active arena, something is broken */
        rc = -2;
        goto out_end_trans;
    }

    /* We've found an active arena, mount it */
    if (logfs_mount_log(logfs, arena_id) != 0) {
        /* Failed to mount the log, something is broken */
        rc = -3;
        goto out_end_trans;
    }

    /* Log has been mounted */
    rc     = 0;

    *fs_id = (uintptr_t)logfs;

out_end_trans:
    logfs->driver->end_transaction(logfs->flash_id);

out_exit:
    return rc;
}

int32_t PIOS_FLASHFS_Logfs_Destroy(uintptr_t fs_id)
{
    int32_t rc;

    struct logfs_state *logfs = (struct logfs_state *)fs_id;

    if (!PIOS_FLASHFS_Logfs_validate(logfs)) {
        rc = -1;
        goto out_exit;
    }

    PIOS_FLASHFS_Logfs_free(logfs);
    rc = 0;

out_exit:
    return rc;
}

/* NOTE: Must be called while holding the flash transaction lock */
static int32_t logfs_garbage_collect(struct logfs_state *logfs)
{
    PIOS_Assert(logfs->mounted);

    /* Source arena is the active arena */
    uint8_t src_arena_id = logfs->active_arena_id;

    /* Compute destination arena */
    uint8_t dst_arena_id = (logfs->active_arena_id + 1) % (logfs->cfg->total_fs_size / logfs->cfg->arena_size);

    /* Erase destination arena */
    if (logfs_erase_arena(logfs, dst_arena_id) != 0) {
        return -1;
    }

    /* Reserve the destination arena so we can start filling it */
    if (logfs_reserve_arena(logfs, dst_arena_id) != 0) {
        /* Unable to reserve the arena */
        return -2;
    }

    /* Copy active slots from active arena to destination arena */
    uint16_t dst_slot_id = 1;
    for (uint16_t src_slot_id = 1;
         src_slot_id < (logfs->cfg->arena_size / logfs->cfg->slot_size);
         src_slot_id++) {
        struct slot_header slot_hdr;
        uintptr_t src_addr = logfs_get_addr(logfs, src_arena_id, src_slot_id);
        if (logfs->driver->read_data(logfs->flash_id,
                                     src_addr,
                                     (uint8_t *)&slot_hdr,
                                     sizeof(slot_hdr)) != 0) {
            return -3;
        }

        if (slot_hdr.state == SLOT_STATE_ACTIVE) {
            uintptr_t dst_addr = logfs_get_addr(logfs, dst_arena_id, dst_slot_id);
            if (logfs_raw_copy_bytes(logfs,
                                     src_addr,
                                     sizeof(slot_hdr) + slot_hdr.obj_size,
                                     dst_addr) != 0) {
                /* Failed to copy all bytes */
                return -4;
            }
            dst_slot_id++;
        }
#ifdef PIOS_INCLUDE_WDG
        PIOS_WDG_Clear();
#endif
    }

    /* Activate the destination arena */
    if (logfs_activate_arena(logfs, dst_arena_id) != 0) {
        return -5;
    }

    /* Unmount the source arena */
    if (logfs_unmount_log(logfs) != 0) {
        return -6;
    }

    /* Obsolete the source arena */
    if (logfs_obsolete_arena(logfs, src_arena_id) != 0) {
        return -7;
    }

    /* Mount the new arena */
    if (logfs_mount_log(logfs, dst_arena_id) != 0) {
        return -8;
    }

    return 0;
}

/* NOTE: Must be called while holding the flash transaction lock */
static int16_t logfs_object_find_next(const struct logfs_state *logfs, struct slot_header *slot_hdr, uint16_t *curr_slot, uint32_t obj_id, uint16_t obj_inst_id)
{
    PIOS_Assert(slot_hdr);
    PIOS_Assert(curr_slot);

    /* First slot in the arena is reserved for arena header, skip it. */
    if (*curr_slot == 0) {
        *curr_slot = 1;
    }

    for (uint16_t slot_id = *curr_slot;
         slot_id < (logfs->cfg->arena_size / logfs->cfg->slot_size);
         slot_id++) {
        uintptr_t slot_addr = logfs_get_addr(logfs, logfs->active_arena_id, slot_id);

        if (logfs->driver->read_data(logfs->flash_id,
                                     slot_addr,
                                     (uint8_t *)slot_hdr,
                                     sizeof(*slot_hdr)) != 0) {
            return -2;
        }
        if (slot_hdr->state == SLOT_STATE_EMPTY) {
            /* We hit the end of the log */
            break;
        }
        if (slot_hdr->state == SLOT_STATE_ACTIVE &&
            slot_hdr->obj_id == obj_id &&
            slot_hdr->obj_inst_id == obj_inst_id) {
            /* Found what we were looking for */
            *curr_slot = slot_id;
            return 0;
        }
#ifdef PIOS_INCLUDE_WDG
        PIOS_WDG_Clear();
#endif
    }

    /* No matching entry was found */
    return -1;
}

/* NOTE: Must be called while holding the flash transaction lock */
/* OPTIMIZE: could trust that there is at most one active version of every object and terminate the search when we find one */
static int8_t logfs_delete_object(struct logfs_state *logfs, uint32_t obj_id, uint16_t obj_inst_id)
{
    int8_t rc;

    bool more = true;
    uint16_t curr_slot_id = 0;

    do {
        struct slot_header slot_hdr;
        switch (logfs_object_find_next(logfs, &slot_hdr, &curr_slot_id, obj_id, obj_inst_id)) {
        case 0:
            /* Found a matching slot.  Obsolete it. */
            slot_hdr.state = SLOT_STATE_OBSOLETE;
            uintptr_t slot_addr = logfs_get_addr(logfs, logfs->active_arena_id, curr_slot_id);

            if (logfs->driver->write_data(logfs->flash_id,
                                          slot_addr,
                                          (uint8_t *)&slot_hdr,
                                          sizeof(slot_hdr)) != 0) {
                rc = -2;
                goto out_exit;
            }
            /* Object has been successfully obsoleted and is no longer active */
            logfs->num_active_slots--;
            break;
        case -1:
            /* Search completed, object not found */
            more = false;
            rc   = 0;
            break;
        default:
            /* Error occurred during search */
            rc = -1;
            goto out_exit;
        }
    } while (more);

out_exit:
    return rc;
}

/* NOTE: Must be called while holding the flash transaction lock */
static int8_t logfs_reserve_free_slot(struct logfs_state *logfs, uint16_t *slot_id, struct slot_header *slot_hdr, uint32_t obj_id, uint16_t obj_inst_id, uint16_t obj_size)
{
    PIOS_Assert(slot_id);
    PIOS_Assert(slot_hdr);

    if (logfs->num_free_slots < 1) {
        /* No free slots to allocate */
        return -1;
    }

    if (obj_size > (logfs->cfg->slot_size - sizeof(slot_hdr))) {
        /* This object is too big for the slot */
        return -2;
    }

    uint16_t candidate_slot_id = (logfs->cfg->arena_size / logfs->cfg->slot_size) - logfs->num_free_slots;
    PIOS_Assert(candidate_slot_id > 0);

    uintptr_t slot_addr = logfs_get_addr(logfs, logfs->active_arena_id, candidate_slot_id);

    if (logfs->driver->read_data(logfs->flash_id,
                                 slot_addr,
                                 (uint8_t *)slot_hdr,
                                 sizeof(*slot_hdr)) != 0) {
        /* Failed to read slot header for candidate slot */
        return -3;
    }

    if (slot_hdr->state != SLOT_STATE_EMPTY) {
        /* Candidate slot isn't empty!  Something is broken. */
        PIOS_DEBUG_Assert(0);
        return -4;
    }

    /* Mark this slot as RESERVED */
    slot_hdr->state       = SLOT_STATE_RESERVED;
    slot_hdr->obj_id      = obj_id;
    slot_hdr->obj_inst_id = obj_inst_id;
    slot_hdr->obj_size    = obj_size;

    if (logfs->driver->write_data(logfs->flash_id,
                                  slot_addr,
                                  (uint8_t *)slot_hdr,
                                  sizeof(*slot_hdr)) != 0) {
        /* Failed to write the slot header */
        return -5;
    }

    /* FIXME: If the header write (above) failed, may have partially written data, thus corrupting that slot but we would have missed decrementing this counter */
    logfs->num_free_slots--;

    *slot_id = candidate_slot_id;
    return 0;
}

/* NOTE: Must be called while holding the flash transaction lock */
static int8_t logfs_append_to_log(struct logfs_state *logfs, uint32_t obj_id, uint16_t obj_inst_id, uint8_t *obj_data, uint16_t obj_size)
{
    /* Reserve a free slot for our new object */
    uint16_t free_slot_id;
    struct slot_header slot_hdr;

    if (logfs_reserve_free_slot(logfs, &free_slot_id, &slot_hdr, obj_id, obj_inst_id, obj_size) != 0) {
        /* Failed to reserve a free slot */
        return -1;
    }

    /* Compute slot address */
    uintptr_t slot_addr   = logfs_get_addr(logfs, logfs->active_arena_id, free_slot_id);

    /* Write the data into the reserved slot, starting after the slot header */
    uintptr_t slot_offset = sizeof(slot_hdr);
    while (obj_size > 0) {
        /* Individual writes must fit entirely within a single page buffer. */
        uint16_t page_remaining = logfs->cfg->page_size - (slot_offset % logfs->cfg->page_size);
        uint16_t write_size     = MIN(obj_size, page_remaining);
        if (logfs->driver->write_data(logfs->flash_id,
                                      slot_addr + slot_offset,
                                      obj_data,
                                      write_size) != 0) {
            /* Failed to write the object data to the slot */
            return -2;
        }

        /* Update our accounting */
        obj_data    += write_size;
        slot_offset += write_size;
        obj_size    -= write_size;
    }

    /* Mark this slot active in one atomic step */
    slot_hdr.state = SLOT_STATE_ACTIVE;
    if (logfs->driver->write_data(logfs->flash_id,
                                  slot_addr,
                                  (uint8_t *)&slot_hdr,
                                  sizeof(slot_hdr)) != 0) {
        /* Failed to mark the slot active */
        return -4;
    }

    /* Object has been successfully written to the slot */
    logfs->num_active_slots++;
    return 0;
}


/**********************************
 *
 * Provide a PIOS_FLASHFS_* driver
 *
 *********************************/
#include "pios_flashfs.h" /* API for flash filesystem */

/**
 * @brief Saves one object instance to the filesystem
 * @param[in] fs_id The filesystem to use for this action
 * @param[in] obj UAVObject ID of the object to save
 * @param[in] obj_inst_id The instance number of the object being saved
 * @param[in] obj_data Contents of the object being saved
 * @param[in] obj_size Size of the object being saved
 * @return 0 if success or error code
 * @retval -1 if fs_id is not a valid filesystem instance
 * @retval -2 if failed to start transaction
 * @retval -3 if failure to delete any previous versions of the object
 * @retval -4 if filesystem is entirely full and garbage collection won't help
 * @retval -5 if garbage collection failed
 * @retval -6 if filesystem is full even after garbage collection should have freed space
 * @retval -7 if writing the new object to the filesystem failed
 */
int32_t PIOS_FLASHFS_ObjSave(uintptr_t fs_id, uint32_t obj_id, uint16_t obj_inst_id, uint8_t *obj_data, uint16_t obj_size)
{
    int8_t rc;

    struct logfs_state *logfs = (struct logfs_state *)fs_id;

    if (!PIOS_FLASHFS_Logfs_validate(logfs)) {
        rc = -1;
        goto out_exit;
    }

    PIOS_Assert(obj_size <= (logfs->cfg->slot_size - sizeof(struct slot_header)));

    if (logfs->driver->start_transaction(logfs->flash_id) != 0) {
        rc = -2;
        goto out_exit;
    }

    if (logfs_delete_object(logfs, obj_id, obj_inst_id) != 0) {
        rc = -3;
        goto out_end_trans;
    }

    /*
     * All old versions of this object + instance have been invalidated.
     * Write the new object.
     */

    /* Check if the arena is entirely full. */
    if (logfs_fs_is_full(logfs)) {
        /* Note: Filesystem Full means we're full of *active* records so gc won't help at all. */
        rc = -4;
        goto out_end_trans;
    }

    /* Is garbage collection required? */
    if (logfs_log_is_full(logfs)) {
        /* Note: Log Full means the log is full but may contain obsolete slots so gc may free some space */
        if (logfs_garbage_collect(logfs) != 0) {
            rc = -5;
            goto out_end_trans;
        }
        /* Check one more time just to be sure we actually free'd some space */
        if (logfs_log_is_full(logfs)) {
            /*
             * Log is still full even after gc!
             * NOTE: This should not happen since the filesystem wasn't full
             *       when we checked above so gc should have helped.
             */
            PIOS_DEBUG_Assert(0);
            rc = -6;
            goto out_end_trans;
        }
    }

    /* We have room for our new object.  Append it to the log. */
    if (logfs_append_to_log(logfs, obj_id, obj_inst_id, obj_data, obj_size) != 0) {
        /* Error during append */
        rc = -7;
        goto out_end_trans;
    }

    /* Object successfully written to the log */
    rc = 0;

out_end_trans:
    logfs->driver->end_transaction(logfs->flash_id);

out_exit:
    return rc;
}

/**
 * @brief Load one object instance from the filesystem
 * @param[in] fs_id The filesystem to use for this action
 * @param[in] obj UAVObject ID of the object to load
 * @param[in] obj_inst_id The instance of the object to load
 * @param[in] obj_data Buffer to hold the contents of the loaded object
 * @param[in] obj_size Size of the object to be loaded
 * @return 0 if success or error code
 * @retval -1 if fs_id is not a valid filesystem instance
 * @retval -2 if failed to start transaction
 * @retval -3 if object not found in filesystem
 * @retval -4 if object size in filesystem does not exactly match buffer size
 * @retval -5 if reading the object data from flash fails
 */
int32_t PIOS_FLASHFS_ObjLoad(uintptr_t fs_id, uint32_t obj_id, uint16_t obj_inst_id, uint8_t *obj_data, uint16_t obj_size)
{
    int8_t rc;

    struct logfs_state *logfs = (struct logfs_state *)fs_id;

    if (!PIOS_FLASHFS_Logfs_validate(logfs)) {
        rc = -1;
        goto out_exit;
    }

    PIOS_Assert(obj_size <= (logfs->cfg->slot_size - sizeof(struct slot_header)));

    if (logfs->driver->start_transaction(logfs->flash_id) != 0) {
        rc = -2;
        goto out_exit;
    }

    /* Find the object in the log */
    uint16_t slot_id = 0;
    struct slot_header slot_hdr;
    if (logfs_object_find_next(logfs, &slot_hdr, &slot_id, obj_id, obj_inst_id) != 0) {
        /* Object does not exist in fs */
        rc = -3;
        goto out_end_trans;
    }

    /* Sanity check what we've found */
    if (slot_hdr.obj_size != obj_size) {
        /* Object sizes don't match.  Not safe to copy contents. */
        rc = -4;
        goto out_end_trans;
    }

    /* Read the contents of the object from the log */
    if (obj_size > 0) {
        uintptr_t slot_addr = logfs_get_addr(logfs, logfs->active_arena_id, slot_id);
        if (logfs->driver->read_data(logfs->flash_id,
                                     slot_addr + sizeof(slot_hdr),
                                     (uint8_t *)obj_data,
                                     obj_size) != 0) {
            /* Failed to read object data from the log */
            rc = -5;
            goto out_end_trans;
        }
    }

    /* Object successfully loaded */
    rc = 0;

out_end_trans:
    logfs->driver->end_transaction(logfs->flash_id);

out_exit:
    return rc;
}

/**
 * @brief Delete one instance of an object from the filesystem
 * @param[in] fs_id The filesystem to use for this action
 * @param[in] obj UAVObject ID of the object to delete
 * @param[in] obj_inst_id The instance of the object to delete
 * @return 0 if success or error code
 * @retval -1 if fs_id is not a valid filesystem instance
 * @retval -2 if failed to start transaction
 * @retval -3 if failed to delete the object from the filesystem
 */
int32_t PIOS_FLASHFS_ObjDelete(uintptr_t fs_id, uint32_t obj_id, uint16_t obj_inst_id)
{
    int8_t rc;

    struct logfs_state *logfs = (struct logfs_state *)fs_id;

    if (!PIOS_FLASHFS_Logfs_validate(logfs)) {
        rc = -1;
        goto out_exit;
    }

    if (logfs->driver->start_transaction(logfs->flash_id) != 0) {
        rc = -2;
        goto out_exit;
    }

    if (logfs_delete_object(logfs, obj_id, obj_inst_id) != 0) {
        rc = -3;
        goto out_end_trans;
    }

    /* Object successfully deleted from the log */
    rc = 0;

out_end_trans:
    logfs->driver->end_transaction(logfs->flash_id);

out_exit:
    return rc;
}

/**
 * @brief Erases all filesystem arenas and activate the first arena
 * @param[in] fs_id The filesystem to use for this action
 * @return 0 if success or error code
 * @retval -1 if fs_id is not a valid filesystem instance
 * @retval -2 if failed to start transaction
 * @retval -3 if failed to erase all arenas
 * @retval -4 if failed to activate arena 0
 * @retval -5 if failed to mount arena 0
 */
int32_t PIOS_FLASHFS_Format(uintptr_t fs_id)
{
    int32_t rc;

    struct logfs_state *logfs = (struct logfs_state *)fs_id;

    if (!PIOS_FLASHFS_Logfs_validate(logfs)) {
        rc = -1;
        goto out_exit;
    }

    if (logfs->mounted) {
        logfs_unmount_log(logfs);
    }

    if (logfs->driver->start_transaction(logfs->flash_id) != 0) {
        rc = -2;
        goto out_exit;
    }

    if (logfs_erase_all_arenas(logfs) != 0) {
        rc = -3;
        goto out_end_trans;
    }

    /* Reinitialize arena 0 */
    if (logfs_activate_arena(logfs, 0) != 0) {
        rc = -4;
        goto out_end_trans;
    }

    /* Mount arena 0 */
    if (logfs_mount_log(logfs, 0) != 0) {
        rc = -5;
        goto out_end_trans;
    }

    /* Chip erased and log remounted successfully */
    rc = 0;

out_end_trans:
    logfs->driver->end_transaction(logfs->flash_id);

out_exit:
    return rc;
}
/**
 * @brief Returs stats for the filesystems
 * @param[in] fs_id The filesystem to use for this action
 * @return 0 if success or error code
 * @retval -1 if fs_id is not a valid filesystem instance
 */
int32_t PIOS_FLASHFS_GetStats(uintptr_t fs_id, struct PIOS_FLASHFS_Stats *stats)
{
    PIOS_Assert(stats);
    struct logfs_state *logfs = (struct logfs_state *)fs_id;

    if (!PIOS_FLASHFS_Logfs_validate(logfs)) {
        return -1;
    }
    stats->num_active_slots = logfs->num_active_slots;
    stats->num_free_slots   = logfs->num_free_slots;
    return 0;
}
#endif /* PIOS_INCLUDE_FLASH */

/**
 * @}
 * @}
 */
